package com.patterns.connection;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ServerConnection extends NetworkConnection implements Runnable {

    private InetAddress addr;
    private Selector selector;
	
    private Map<SocketChannel,Listener> listeners;
	
	public ServerConnection(int port) {
		super(port);
		listeners = new ConcurrentHashMap<>();
		myThread = new Thread(this);
        myThread.start();
	}
	
	public ServerConnection(){
		super();
	}

	@Override
	public void run() {
        try {
            this.selector = Selector.open();
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            serverChannel.configureBlocking(false);
                            
            InetSocketAddress listenAddr = new InetSocketAddress(this.addr, this.port);
            serverChannel.socket().bind(listenAddr);
            serverChannel.register(this.selector, SelectionKey.OP_ACCEPT);
                            
            while (!Thread.currentThread().isInterrupted()) {
                this.selector.select();
                
                Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
                while (keys.hasNext()) {
                    SelectionKey key = (SelectionKey) keys.next();
                    keys.remove();
                    
                    if (! key.isValid()) {
                        continue;
                    }
                    
                    if (key.isAcceptable() && key!=null) {
                        this.accept(key);
                    }

                    else if (key.isWritable()) {
                        this.write(key);
                    }
                }
            }
            
	    } catch (IOException e) {
	            e.printStackTrace();
	            this.shutDown();
	    }
	}

	private void write(SelectionKey key) {
        SocketChannel channel = (SocketChannel) key.channel();
        Listener currentListener = listeners.get(channel);
        if ( currentListener!=null && currentListener.isChanged()) {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
	        try {
				ObjectOutputStream oos = new ObjectOutputStream(bos);
				oos.writeObject(currentListener.getCache());
				oos.flush();
				channel.write(ByteBuffer.wrap(bos.toByteArray()));
				currentListener.disableChanged();
			} catch (IOException e) {
				e.printStackTrace();
				listeners.remove(channel);
	            key.cancel();
	            return;
			}
		        
        }
		
	}

	private void accept(SelectionKey key) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        SocketChannel channel = serverChannel.accept();
        channel.configureBlocking( false );
        channel.register( selector, SelectionKey.OP_WRITE);
        listeners.put(channel, new Listener());
        System.out.println("Client connected");
	}

	@Override
	public void shutDown() {
		myThread.interrupt();
	}

	public Collection<Listener> getListeners() {
		return listeners.values();
	}

}
